Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add withChecks step #49

Merged
merged 11 commits into from
Dec 20, 2020
Merged

Add withChecks step #49

merged 11 commits into from
Dec 20, 2020

Conversation

XiongKezhi
Copy link
Contributor

@XiongKezhi XiongKezhi commented Nov 29, 2020

Linked #45

cc @mrginglymus @jglick

@XiongKezhi XiongKezhi added the enhancement New feature or request label Nov 29, 2020
@XiongKezhi XiongKezhi requested a review from timja November 29, 2020 12:36
@XiongKezhi XiongKezhi self-assigned this Nov 29, 2020
* Constructor used for pipeline by Stapler.
*/
@DataBoundConstructor
public WithChecksStep(final String name) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it can take more than one, normally you would use a databoundsetter for any optional parameters

Then the syntax would be:

withChecks('Tests') {
...
}
withChecks(name: 'Tests', myotherparam: 'hello') {
...
}

if there's only one mandatory field in constructor then you can omit the parameter name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should we make all params optional in case that in the future, some users may only want to inject a certain field like conclusion instead of name?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we do that then the user will have to specify the name in a more verbose way,

withChecks(name: 'hello') rather than withChecks('hello')

Copy link
Contributor Author

@XiongKezhi XiongKezhi Dec 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify the name wouldn't be annoying for users IMO but make it mandatory will be an issue for future changes?

Also, it seems the snippet generator will always specify name

@mrginglymus What do you think? Do you think there will be situations where you do not want to specify the name but the status (e.g. publish several checks in the same status (say in progress) but with different names in the closure)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be fine to be mandatory

@mrginglymus
Copy link
Contributor

With the usual caveat of
image

I had a brief play with this myself and got distracted with also doing the update bits too* - I couldn't see an easy way to do it without changing the API of ChecksPublisher to look something like:

public abstract class ChecksPublisher {

    private final Job<?, ?> job;

    public ChecksPublisher(Job<?, ?> job) {
        this.job = job;
    }

    protected abstract void doPublish(ChecksDetails details)  // replaces existing `publish`

    public final void publish(ChecksDetails details) {
        doPublish(details);
        details.getName().ifPresent(name -> job.addOrReplaceAction(new ChecksInfo(name, details.getConsclusion()));
    }

    public ChecksConclusion getConclusion(String checksName) {
        return job.getActions(ChecksInfo.class)
                .stream()
                .filter(a -> a.getName().equals(checksName))
                .findFirst()
                .map(ChecksInfo::getConclusion)
                .orElse(ChecksConclusion.None);
    }
}

ie, ChecksInfo was an InvisibleAction, and there was a separate WithChecksContext that stored the current name.

The WithChecksStep then implements a BodyExecutionCallback that looks something like:

private static final class Callback extends BodyExecutionCallback {
    private static final long serialVersionUID = 1L;

    private ChecksPublisher getPublisher(StepContext context) throws IOException, InterruptedException {
        return ChecksPublisherFactory.fromRun(context.get(Run.class), context.get(TaskListener.class));
    }

    private WithCheckContext getCheckContext(StepContext context) throws IOException, InterruptedException {
        return context.get(WithCheckContext.class);
    }

    private ChecksDetails getDetails(WithCheckContext context, ChecksConclusion conclusion) {
        return new ChecksDetails.ChecksDetailsBuilder()
                .withName(context.getName())
                .withConclusion(conclusion)
                .withStatus(ChecksStatus.COMPLETED)
                .build();
    }

    @Override
    public void onStart(StepContext context) {
        try {
            ChecksDetails details = new ChecksDetails.ChecksDetailsBuilder()
                    .withName(getCheckContext(context).getName())
                    .withStatus(ChecksStatus.IN_PROGRESS)
                    .build();
            getPublisher(context).publish(details);
        } catch (Exception e) {
            context.onFailure(e);
        }
    }

    @Override
    public void onSuccess(StepContext context, Object result) {
        try {
            WithCheckContext checkContext = getCheckContext(context);
            ChecksPublisher publisher = getPublisher(context);
            if (publisher.getConclusion(checkContext.getName()) == ChecksConclusion.NONE) {
                publisher.publish(getDetails(checkContext, ChecksConclusion.SUCCESS));
                context.onSuccess(null);
            }
        } catch (Exception e) {
            context.onFailure(e);
        }
    }

    @Override
    public void onFailure(StepContext context, Throwable t) {
        try {
            WithCheckContext checkContext = getCheckContext(context);
            ChecksPublisher publisher = getPublisher(context);
            if (t instanceof FlowInterruptedException && ((FlowInterruptedException) t).isActualInterruption()) {
                publisher.publish(getDetails(checkContext, ChecksConclusion.CANCELED));
            } else if (publisher.getConclusion(checkContext.getName()) != ChecksConclusion.FAILURE) {
                publisher.publish(getDetails(checkContext, ChecksConclusion.FAILURE));
            }
            context.onFailure(t);
        } catch (Exception e) {
            context.onFailure(e);
        }
    }
}

Obviously the API change is sub-optimal, but at least it only affects implementations of this API (of which we have full control :)) rather than consumers of it (which we do not :()

* it's somewhat scope creep, but I wondered if the ability to update a check should be an implementation detail consumers of the checks api plugin, or something provided by it - e.g. by storing the external id associated with a checks name in this new ChecksInfo class. If so, now might be the ideal time to do that.

@XiongKezhi
Copy link
Contributor Author

Instead of storing the published checks locally, have you ever thought about query them from GitHub? Maybe use the commit: https://docs.github.com/en/free-pro-team@latest/rest/reference/checks#list-check-runs-for-a-git-reference.

This needs extra requests to GitHub, but seems not a not big deal?

@mrginglymus
Copy link
Contributor

mrginglymus commented Nov 29, 2020

Yeah, I think that would be neater for the most part - means that we're not attempting to mirror github's state and hoping we've got it right...

We would still need to add a new API to the publisher though, possibly, for onSuccess and onFailure to get the current state of the check; so if we add an

    abstract public ChecksConclusion getConclusion(String checksName)

to ChecksPublisher then implementations can either attempt to store that information locally, or fetch it from the external source.

This could, I guess, return ChecksConclusion.NONE by default...

@mrginglymus
Copy link
Contributor

Though the comment

Note: The Checks API only looks for pushes in the repository where the check suite or check run were created. Pushes to a branch in a forked repository are not detected and return an empty pull_requests array.

doesn't inspire me with confidence...

@XiongKezhi
Copy link
Contributor Author

We would still need to add a new API to the publisher though, possibly, for onSuccess and onFailure to get the current state of the check;

But if we store the info of the check as an invisible action to the run, couldn't we directly resolve it in StepContext?

doesn't inspire me with confidence...

Yeah, seems a problem; I'll test it.

@XiongKezhi
Copy link
Contributor Author

XiongKezhi commented Nov 30, 2020

@mrginglymus I test and it actually lists check runs of the PR from a forked repo: XiongKezhi/codingstyle#13 \o/
截屏2020-11-30 下午4 28 52

I think the reason may be: the commits of a PR (filed from a forked repo or not) in the repository tracked by Jenkins are indeed the "pushes in the repository where the check suite or check run were created" as the GitHub doc mentions.

Do you have time to double-check it? Maybe in your repos.

@mrginglymus
Copy link
Contributor

I think the reason may be: the commits of a PR (filed from a forked repo or not) in the repository tracked by Jenkins are indeed the "pushes in the repository where the check suite or check run were created" as the GitHub doc mentions.

That does make sense (otherwise how would you create check runs) - but then I wonder what the note is about...

@mrginglymus
Copy link
Contributor

mrginglymus commented Dec 1, 2020

So as I see it there are two problems, each with the same two options:

  1. Tracking the 'id' of an check so we can update it
    a. we could do this in the checks-api by storing a mapping of String name: long id
    b. we could defer this to the implementation, which could either do its own tracking of the id as in a., or it could look up the id on demand
  2. Tracking the status of a check so we know whether or not it has been completed at the end of the withChecks block
    a. we could do this in the checks-api by storing a mapping of String name: ChecksConclusion conclusion
    b. we could defer this to the implementation, which could either do its own tracking of the status as in a., or it could look up the conclusion on demand

For 1., I think b. (doing it within the implementation) seems like it might make more sense as it allows each implementation to have its own understanding of what 'updating' actually means, and minimises api changes for checks-api - you just continue to call publish and it's up to the implementation to work out if that means an update or a create.

For 2., I think b. also might make sense, again in the interest of minimising changes to the API of the checks-api - just adding a getConclusion(String name) to the ChecksPublisher interface would allow us to implement everything necessary here. We can then decide later on whether we want to track the id/conclusions in InvisibleActions or by fetching on demand, without having to change all the APIs.

I might throw up a quick PR for github-checks-api just to put into code the above waffle, as I think it'll make more sense :D

edit: example PR here: jenkinsci/github-checks-plugin#87

@XiongKezhi XiongKezhi marked this pull request as ready for review December 14, 2020 17:05
@XiongKezhi XiongKezhi requested a review from uhafner December 14, 2020 17:05
@XiongKezhi
Copy link
Contributor Author

cc @mrginglymus (cannot request your review directly in the reviewers, maybe because you are not in the jenkins org)

Copy link
Member

@uhafner uhafner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just the pom suppressions should be removed.

pom.xml Outdated Show resolved Hide resolved
Copy link
Member

@timja timja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good except for suppression on version

Copy link
Member

@timja timja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm are we missing some documentation for this feature? what does this look like in snippet generator too?

@XiongKezhi
Copy link
Contributor Author

Snippet Generator Preview:

截屏2020-12-16 下午4 53 34

README.md Show resolved Hide resolved
@mrginglymus
Copy link
Contributor

Hooray!

This is great, and will hopefully make it a lot easier to have all the plugins have overridable checks names without complicated configuration options on each.

Should we add support for withChecks to publishChecks? As mentioned in other comments, I'm hoping to have things like:

withChecks('Python Tests') {
    publishChecks status: IN_PROGRESS
    try {
        sh 'pytest tests'
        junit 'results.xml'
    } catch (FlowInterruptedException err) {
        publishChecks status: CANCELLED
        throw err
   } catch( Exception err) {
       publishChecks status: FAILED
       throw err
   }
}

It would also help serve as a guide to developers on how to use it.

Do you think that the above pattern might be something we could build into the withChecks step? AFICT it should be doable by implementing a BodyExecutionCallback listener, though I appreciate that perhaps it's too much heavy lifting for a single command. Maybe as a further extension in a separate PR?

@XiongKezhi
Copy link
Contributor Author

Should we add support for withChecks to publishChecks?

I think we should.

Do you think that the above pattern might be something we could build into the withChecks step? Maybe as a further extension in a separate PR?

I think we should do that and it's ok to do it in this PR? @timja

And if we do that, we will set the name as a mandatory field, or we don't know what to publish when the exceptions occur.
So for double check, Do you think there will be situations where you do not want to specify the name but only the status (e.g. publish several checks in the same status (say in progress) but with different names in the closure)?

@timja
Copy link
Member

timja commented Dec 16, 2020

Should we add support for withChecks to publishChecks?

I think we should.

Do you think that the above pattern might be something we could build into the withChecks step? Maybe as a further extension in a separate PR?

I think we should do that and it's ok to do it in this PR? @timja

Yes sounds good

And if we do that, we will set the name as a mandatory field, or we don't know what to publish when the exceptions occur.
So for double check, Do you think there will be situations where you do not want to specify the name but only the status (e.g. publish several checks in the same status (say in progress) but with different names in the closure)?

If you want different names then use a different closure

@mrginglymus
Copy link
Contributor

What is the expected behaviour if you nest withChecks calls? Presumably it's something we don't support (and should explicitly 💥 on)?

@@ -19,8 +19,7 @@

<properties>
<java.level>8</java.level>
<revision>1.1.2</revision>
<changelist>-SNAPSHOT</changelist>
<revision>1.2.0</revision>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still should be a SNAPSHOT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sem-ver plugin will warn about compatibility if add SNAPSHOT

@XiongKezhi
Copy link
Contributor Author

XiongKezhi commented Dec 17, 2020

Seems the injected ChecksInfo cannot be accessed inside the callback? @timja @mrginglymus

context.get(ChecksInfo.class) returns nothing.

I can directly pass the ChecksInfo to the callback class while constructing if that is the case.

@timja
Copy link
Member

timja commented Dec 17, 2020

Not sure

pom.xml Outdated Show resolved Hide resolved
* Constructor used for pipeline by Stapler.
*/
@DataBoundConstructor
public WithChecksStep(final String name) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be fine to be mandatory


@Override
public void onSuccess(final StepContext context, final Object result) {
context.onSuccess(result);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the support of the callback, this step may be much more useful (but I'm not sure if that goes beyond this step's scope) than just injecting the properties: it can be used as a progress reporter for the steps in Its closure.

Then, here is an issue:

  1. We cannot publish checks on success since we definitely don't want a simple success report check to overwrite what's inside the block (in the context of this API, we don't know about checks updating even if we support that in implementation).

  2. If we don't publish success, however, a simple closure in which no step publishes checks using the injected name will leave the check uncompleted forever. (as said before, if the progress reporter feature goes beyond the scope, then this should the users' problem)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't publish success, however, a simple closure in which no step publishes checks using the injected name will leave the check uncompleted forever. (as said before, if the progress reporter feature goes beyond the scope, then this should the users' problem)

not our problem I think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's reasonable to expect users to be responsible for sending a success message (or indeed a failure message) within their invocation of withChecks. I did wonder about adding to the publisher API the ability to get the current conclusion (https://github.com/jenkinsci/github-checks-plugin/pull/87/files#diff-9c475e3093c11ae9324debf6c824b9c15277a027428e5c7ed60ad6ac2ac25284R131-R134) so that we could query that in onSuccess and send a generic 'success' check if nothing else was set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems not feasible, since in the context of the API, we don't expect the implementation to possess the ChecksDetails alone the build, and if we take that as a users' responsible, there is no need to add the getConclusion method only for this feature IMO.

docs/consumers-guide.md Outdated Show resolved Hide resolved
README.md Show resolved Hide resolved
@XiongKezhi XiongKezhi force-pushed the implement_with_checks_step branch from dc89d62 to 86dc8e3 Compare December 19, 2020 08:34
@XiongKezhi XiongKezhi force-pushed the implement_with_checks_step branch from 86dc8e3 to e724c04 Compare December 19, 2020 08:51
@XiongKezhi XiongKezhi merged commit 525178f into master Dec 20, 2020
@XiongKezhi XiongKezhi deleted the implement_with_checks_step branch December 20, 2020 06:25
XiongKezhi added a commit that referenced this pull request Dec 20, 2020
* Add withChecks step

* Add test for WithChecksStep.

* Add document and snippet generator support.

* Bump version to 1.2.0 for adding ChecksInfo and WithChecksStep.

* remove unused setter

* Remove unused import

* Add callback for withChecks.

* Add consumer doc.

* Remove unnessary denpendency version in pom

* Improve consumer doc
@mrginglymus mrginglymus mentioned this pull request Dec 20, 2020
uhafner added a commit that referenced this pull request Dec 20, 2020
Due to #49 the
<changelist> tag has been removed which breaks the build.
Additionally, RevApi needs to be configured to not pick up
releases from the incrementals repository.
XiongKezhi pushed a commit that referenced this pull request Dec 21, 2020
Due to #49 the
<changelist> tag has been removed which breaks the build.
Additionally, RevApi needs to be configured to not pick up
releases from the incrementals repository.
uhafner added a commit to jenkinsci/warnings-ng-plugin that referenced this pull request Dec 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants